local addon = LibStub("AceAddon-3.0"):GetAddon("Poppins")

local LibStickyFrames = LibStub("LibStickyFrames-2.0")
local LibMacroParser = LibStub("LibMacroParser-1.0")
local LibKeyBound = LibStub("LibKeyBound-1.0")

local count = 0
local weak = {__mode='k'}
local cache = setmetatable({}, weak)
local used = setmetatable({}, weak)

local Frame = {}
addon.Frame = Frame
local prototype = CreateFrame("Frame")
LibStub("AceEvent-3.0"):Embed(prototype)
Frame.prototype = prototype
Frame.metatable = { __index = prototype }
Frame.callbacks = LibStub("CallbackHandler-1.0"):New(Frame)

function Frame:Retrieve(id, settings, option)
	local frame = next(cache)
	if frame then
		cache[frame] = nil
	else
		count = count + 1
		local rid = count
		local header = addon.header
		local frameName = format("%sFrame%d", addon.name, rid)
		frame = setmetatable(CreateFrame("Frame", frameName, header, "SecureStateHeaderTemplate"), addon.Frame.metatable)
		frame.rid = rid
		frame:SetWidth(36)
		frame:SetHeight(36)

		frame:SetBackdrop({bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", tile = true, tileSize = 16, insets = {left = 0, right = 0, top = 0, bottom = 0},})
		frame:SetBackdropColor(0, 1, 1, 0)

		frame.attributes = {}
		frame:HookScript("OnAttributeChanged", frame.OnAttributeChanged)

		local buttonName = format("%sButton", frameName)
		local button = setmetatable(CreateFrame("CheckButton", buttonName, frame, "SecureAnchorEnterTemplate,SecureActionButtonTemplate,ActionButtonTemplate"), addon.Button.metatable)
		frame.button = button
		button.parent = frame
		button:SetAllPoints(frame)
	
		button.icon = _G[format("%sIcon", buttonName)]
		button.border = _G[format("%sBorder", buttonName)]
		button.cooldown = _G[format("%sCooldown", buttonName)]
		button.macroName = _G[format("%sName", buttonName)]
		button.hotkey = button:CreateFontString(format("%sHotKey", buttonName), "ARTWORK", "NumberFontNormal") -- _G[format("%sHotKey", buttonName)]
		button.selected = button:CreateFontString(format("%sSelected", buttonName), "ARTWORK", "NumberFontNormal")
		button.count = _G[format("%sCount", buttonName)]
		button.flash = _G[format("%sFlash", buttonName)]
		button.flash:Hide()

		button:SetAttribute("anchorchild", header)
		frame:SetAttribute("addchild", button)
		button:SetAttribute("checkselfcast", true)
	
		button.attributes = {}
		button:HookScript("OnAttributeChanged", button.OnAttributeChanged)
		button:HookScript("OnEnter", button.OnEnter)
		button:HookScript("OnLeave", button.OnLeave)
		button:RegisterForClicks("LeftButtonUp", "RightButtonUp")

		frame.popups = {}
	end
	used[frame] = true

	if id then frame:SetID(id) end
	if settings then frame:SetSettings(settings) end
	if options then frame:SetOptions(options) end

	return frame
end

function Frame:Iterate()
	return pairs(used)
end

function prototype:Super(object, ...)
	return getmetatable(self).__index[object](self, ...)
end

function prototype:SetID(id)
	self.id = id

	local rid = self.rid
	local header = addon.header
	local button = self.button

	if id then
		LibStickyFrames:RegisterFrame(self)
		LibStickyFrames.RegisterCallback(self, "OnStopFrameMoving")
		LibStickyFrames:SetFrameEnabled(self, true)
		LibStickyFrames:SetFrameInsets(self, -3, -2, -2, -3)
		LibStickyFrames:SetFrameText(self, tostring(id))
	
		button.state = ""
		button:SetScript("OnShow", button.OnShow)
		button:SetScript("OnHide", button.OnHide)
		button:SetScript("OnDragStart", button.OnDragStart)
		button:SetScript("OnReceiveDrag", button.OnReceiveDrag)
		button:SetScript("PostClick", button.OnClick)
		button:RegisterAllEvents()
	
		header:SetAttribute("statemap-anchor-show"..rid, "*:"..id)
		header:SetAttribute("statemap-anchor-hide"..rid, "")
		header:SetAttribute("delaystatemap-anchor-hide"..rid, id..":0")
		header:SetAttribute("delaytimemap-anchor-hide"..rid,  id..":0.5")
		header:SetAttribute("delayhovermap-anchor-hide"..rid, id..":true")

		if self.settings then self:UpdateSettings() end
	else
		if self.settings then self:UpdateSettings() end

		LibStickyFrames:UnregisterFrame(self)
		LibStickyFrames.UnregisterAllCallbacks(self)
		Frame.UnregisterAllCallbacks(self)

		button:UnregisterAllEvents()
		button:SetScript("OnShow", nil)
		button:SetScript("OnHide", nil)
		button:SetScript("OnDragStart", nil)
		button:SetScript("OnReceiveDrag", nil)
		button:SetScript("OnUpdate", nil)
		button:SetScript("PostClick", nil)
		LibMacroParser.UnregisterAllCallbacks(button)
	
		for name, value in pairs(self.attributes) do
			self:SetAttribute(name, nil)
		end
		for name, value in pairs(button.attributes) do
			button:SetAttribute(name, nil)
		end
	end

	Frame.callbacks:Fire("OnSetID", self, id)
end

function prototype:SetSettings(settings)
	self.settings = settings
	self:UpdateSettings()

	Frame.callbacks:Fire("OnSetSettings", self, settings)
end

function prototype:UpdateSettings()
	local id = self.id
	local settings = self.settings

	if id and settings then
		local statebutton = ""
		for i=1, settings.popups.count do
			statebutton = format("%s%d:%d;", statebutton, i, i)
		end
		self.button:SetAttribute("*statebutton*", statebutton)
		local state = statebutton:match(format("(%s):%%d-;", settings.state)) or "1"
		self:SetAttribute("state", state)

		self.button:Update()
		self:UpdateAllPopups()
		if self.options then
			self:UpdateOptions()
		else
			self:UpdatePosition()
		end
	else
		if self.options then
			self:UpdateOptions()
		else
			LibStickyFrames:SetFramePoints(self)
			self:ClearAllPoints()
			self:Hide()
		end
		for i=#self.popups, 1, -1 do
			self:RemovePopup(i)
		end
		self.button:Update()

		self:SetAttribute("state", nil)
		self.button:SetAttribute("*statebutton*", nil)
	end
end

function prototype:SetOptions(options)
	self.options = options
	self:UpdateOptions()

	Frame.callbacks:Fire("OnSetOptions", self, options)
end

function prototype:UpdateOptions()
	local id = self.id
	local settings = self.settings
	local options = self.options

	if id and settings and options then
		LibButtonFacade = LibButtonFacade or LibStub("LibButtonFacade", true)
		if LibButtonFacade then
			LibButtonFacade:Group(addon.name, settings.group or tostring(id)):Skin(unpack(options.skin))
		end
		self:SetAlpha(options.alpha)
		self:SetScale(options.scale)
		self:UpdatePosition()
		self:SetLock(addon:IsLocked())

		self.button:UpdateOptions()
		for id, button in ipairs(self.popups) do
			button:UpdateOptions()
		end
	else
		for id=#self.popups, 1, -1 do
			self.popups[id]:UpdateOptions()
		end
		self.button:UpdateOptions()

		self:SetScale(1)
		if settings then
			self:UpdatePosition()
		end

		LibButtonFacade = LibButtonFacade or LibStub("LibButtonFacade", true)
		if LibButtonFacade then
			LibButtonFacade:DeleteGroup(addon.name, settings.group or tostring(id))
		end
	end
end

function prototype:GetOption(option)
	local options = self.options
	if type(option) == "table" then
		for i, v in ipairs(option) do
			options = options[v]
		end
		return options
	else
		return options[option]
	end
end

function prototype:SetOption(option, value)
	if self:GetGroup() then return end
	local options = self.options
	if type(option) == "table" then
		for i, v in ipairs(option) do
			if i == #option then
				options[v] = value
			else
				options = options[v]
			end
		end
	else
		options[option] = value
	end
	self:UpdateOptions()
end

function prototype:ResetOptions()
	if self:GetGroup() then return end
	addon.db.profile.groups[self.id] = nil
	self.options = addon.db.profile.groups[self.id]
	self:UpdateOptions()
end

function prototype:Store()
	used[self] = nil
	self:SetOptions(nil)
	self:SetSettings(nil)
	self:SetID(nil)
	cache[self] = true
end

function prototype:UpdateAllPopups()
	local popups = self.settings.popups
	for id = popups.count + 1, #self.popups do
		self:RemovePopup(id)
	end
	for id = 1, popups.count do
		self:UpdatePopup(id)
	end
end

function prototype:NewPopup()
	local settings = self.settings.popups
	settings.count = settings.count + 1
	self:UpdatePopup(settings.count)
end

function prototype:DeletePopup(id)
	local settings = self.settings.popups
	for i=1, (settings.count - 1) do
		if i >= id then
			settings[i] = settings[i + 1]
			i = i + 1
		end
	end
	settings[settings.count] = nil
	settings.count = settings.count - 1
	self:UpdateAllPopups()
end

function prototype:UpdatePopup(id)
	local settings = self.settings.popups[id]
	local popup = self.popups[id]
	if not popup then
		popup = addon.Popup:Retrieve(id, self)
		self.popups[id] = popup
	end

	popup:SetActions(settings)
	self:UpdateHover()

	return popup
end

function prototype:RemovePopup(id)
	self.popups[id] = self.popups[id]:Store()
	self:UpdateHover()
end

function prototype:GetPopup(id)
	return self.popups[id]
end

function prototype:UpdateHover()
	local button = self.button
	if #self.popups <= 1 and addon.db.profile.lock then
		button:SetAttribute("*childstate-onenter", nil)
		button:SetAttribute("*childstate-onleave", nil)
	else
		local rid = self.rid
		button:SetAttribute("*childstate-onenter", format("show%d", rid))
		button:SetAttribute("*childstate-onleave", format("hide%d", rid))
	end
end

function prototype:GetGroup()
	return self.settings.group
end

function prototype:SetGroup(group)
	if group and type(group) ~= "string" then return end
	local id = self.id
	local settings = self.settings
	if not group and settings.group then
		addon:CopyOptions(settings.group, id)
	end
	settings.group = group
	self.options = addon.db.profile.groups[group or id]
	self:UpdateOptions()
end

function prototype:StateChanged(newState)
	if newState == "nil" then return end

	local oldState = self.settings.state
	self.settings.state = newState
	local button = self.button
	button.state = "-"..newState
	button:Update()

	Frame.callbacks:Fire("OnStateChanged", self, newState, oldState)
end

function prototype:UpdatePosition()
	local s = self:GetEffectiveScale()
	local x, y, frame, pointA, pointB = unpack(self.settings.position)

	if frame then
		local button = addon.buttons[frame]
		if button then
			LibStickyFrames:SetFramePoints(self, pointA, button, pointB, x, y)
		else
			Frame.RegisterCallback(self, "OnUpdatePosition")
			return
		end
	else
		LibStickyFrames:SetFramePoints(self)
		x, y = x and x / s, y and y / s
		self:ClearAllPoints()
		self:SetPoint(x and "TOPLEFT" or "CENTER", UIParent, y and "BOTTOMLEFT" or "CENTER", x or math.random(-150, 150), y or math.random(-150, 150))
		if not x or not y then
			self.settings.position = { self:GetLeft() * s, self:GetTop() * s }
		end
	end
	self:Show()
	Frame.callbacks:Fire("OnUpdatePosition", self)
end

function prototype:OnUpdatePosition(event, frame)
	if frame ~= self and self.settings.position[3] == frame.id then
		Frame.UnregisterAllCallbacks(self)
		self:UpdatePosition()
	end
end

local relPoints = { TOP = "BOTTOM", BOTTOM = "TOP", LEFT = "RIGHT", RIGHT = "LEFT" }
function prototype:SetPosition(x, y, relId, pointA, pointB)
	local position = self.settings.position
	position[1] = x or false
	position[2] = y or false
	position[3] = relId or false
	local pointA = relId and (pointA or "RIGHT") or false
	position[4] = pointA
	position[5] = pointA and (pointB or relPoints[pointA]) or false
	self:UpdatePosition()
end

function prototype:GetPosition()
	return unpack(self.settings.position)
end

function prototype:Anchor()
	local s = self:GetEffectiveScale()
	self:SetPosition(self:GetLeft() * s, self:GetTop() * s)
end

function prototype:SetLock(locked)
	LibStickyFrames:SetFrameHidden(self, not locked)
	self:SetMovable(locked)
	self.button:EnableMouse(not locked)

	Frame.callbacks:Fire("OnSetLock", self, locked)
	for id, button in ipairs(self.popups) do
		button:SetLock(locked)
	end
	self:UpdateColor()
end

function prototype:OnStopFrameMoving(event, frame, pointA, frameB, pointB, x, y)
	if frame ~= self then return end
	
	if used[frameB] and pointA and pointB then
		frameB = frameB.id
	else
		local s = self:GetEffectiveScale()
		x, y = self:GetLeft() * s, self:GetTop() * s
		frameB, pointA, pointB = nil, nil, nil
	end
	self:SetPosition(x, y, frameB, pointA, pointB)
end

function prototype:OnAttributeChanged(name, value)
	self.attributes[name] = value
end

--LibKeyBound related functions
local backdropColor = { 1, 1, 0, 0.5}
function prototype:UpdateColor()
	if LibStickyFrames:GetGroup() == true then
		self:SetFrameStrata("DIALOG")
		self.button:SetFrameStrata("LOW")
		self:SetBackdropColor(0, 0, 0, 0)
	elseif LibKeyBound:IsShown() then
		self:SetFrameStrata("DIALOG")
		self.button:SetFrameStrata("LOW")
		self:SetBackdropColor(LibKeyBound:GetColorKeyBoundMode())
	else
		self:SetFrameStrata("BACKGROUND")
		self.button:SetFrameStrata(self.settings and self.settings.strata or "BACKGROUND")
		self:SetBackdropColor(0, 0, 0, 0)
	end
end